--[[

    Cry of Fear Gameplay.
    Version 04/20/2019.

    Description:
    Cry of Fear gameplay system for CSO Studio.


        MIT License

        Copyright (c) 2019 Anggara Yama Putra

        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:

        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.

        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.

]]


-- Local variables.
local debug = COF.Debug;
local module = COF.UI;
local moduleScreenFade = module.ScreenFade;
local moduleChapter = module.Chapter;
moduleChapter.Part = nil;
moduleChapter.Title = nil;
local modulePhone = module.Phone;
local moduleHealth = module.Health;
local moduleStamina = module.Stamina;
local moduleCutscene = module.Cutscene;
local moduleSubtitle = module.Subtitle;
local moduleHint = module.Hint;
local moduleObjective = module.Objective;
moduleObjective.Description = nil;
local modulePrologue = module.Prologue;
local moduleEpilogue = module.Epilogue;
local moduleCredit = module.Credit;
local moduleEvent = module.Event;
local syncvalue = COF.SyncValue;
local enum = COF.Enum;
local box = module.Box;
local text = module.Text;
local size = UI.ScreenSize();
local center = { x = size.width/2 , y = size.height/2 };
local isCutscene = false;
local isPhoneAvailable = true;
local isPhoneVisible = false;
local isPrologue = false;
local isEpilogue = false;
local isCredit = false;
local lastPressedPhoneToggleTime = 0;
local lastPressedObjectiveToggleTime = 0;
local lastPhoneVisibilityOnCutscene = nil;
local lastStamina = 0;
local doEpilogueSendSignalOff = false;
local doCreditSendSignalOff = false;


-- Init SyncValue(s).
syncvalue.Chapter.Instance = UI.SyncValue.Create( syncvalue.Chapter.NAME );
syncvalue.Phone.Available.Instance = UI.SyncValue.Create( syncvalue.Phone.Available.NAME );
syncvalue.Phone.Message.Instance = UI.SyncValue.Create( syncvalue.Phone.Message.NAME );
syncvalue.Objective.Instance = UI.SyncValue.Create( syncvalue.Objective.NAME );
do
    local i = UI:PlayerIndex();
    syncvalue.ScreenFade.Instance = UI.SyncValue.Create( string.format( syncvalue.ScreenFade.NAME , i ) );
    syncvalue.Health.Instance = UI.SyncValue.Create( string.format( syncvalue.Health.NAME , i ) );
    syncvalue.MaxHealth.Instance = UI.SyncValue.Create( string.format( syncvalue.MaxHealth.NAME , i ) );
    syncvalue.Stamina.Instance = UI.SyncValue.Create( string.format( syncvalue.Stamina.NAME , i ) );
    syncvalue.Cutscene.Instance = UI.SyncValue.Create( string.format( syncvalue.Cutscene.NAME , i ) );
    syncvalue.Subtitle.Instance = UI.SyncValue.Create( string.format( syncvalue.Subtitle.NAME , i ) );
    syncvalue.Hint.Instance = UI.SyncValue.Create( string.format( syncvalue.Hint.NAME , i ) );
    syncvalue.Prologue.Instance = UI.SyncValue.Create( string.format( syncvalue.Prologue.NAME , i ) );
    syncvalue.Epilogue.Instance = UI.SyncValue.Create( string.format( syncvalue.Epilogue.NAME , i ) );
    syncvalue.Credit.Instance = UI.SyncValue.Create( string.format( syncvalue.Credit.NAME , i ) );
end


-- Init box(es).
box.ScreenFade.Instance = UI.Box.Create();
box.ScreenFade.Config.fademode = enum.FADEMODE.SET;
box.ScreenFade.Config.faderate = box.ScreenFade.Config.DEFAULT.faderate;
box.ScreenFade.Config.r = box.ScreenFade.Config.DEFAULT.r;
box.ScreenFade.Config.g = box.ScreenFade.Config.DEFAULT.g;
box.ScreenFade.Config.b = box.ScreenFade.Config.DEFAULT.b;
box.ScreenFade.Config.a = box.ScreenFade.Config.DEFAULT.a;
box.ScreenFade.Config.rDrawed = box.ScreenFade.Config.r;
box.ScreenFade.Config.gDrawed = box.ScreenFade.Config.g;
box.ScreenFade.Config.bDrawed = box.ScreenFade.Config.b;
box.ScreenFade.Config.aDrawed = 0;
box.ScreenFade.Instance:Set( box.ScreenFade.Config );
box.ScreenFade.Instance:Set( { a = box.ScreenFade.Config.aDrawed } );
box.Chapter.Instance = UI.Box.Create();
box.Chapter.Config.width = 0;
box.Chapter.Config.a = 255;
box.Chapter.Instance:Set( box.Chapter.Config );
box.Phone.Screen.Instance = UI.Box.Create();
box.Phone.Border.Instances = 
{
    UI.Box.Create() -- Top message.
    , UI.Box.Create() -- Bottom message.
    , UI.Box.Create() -- Top screen.
    , UI.Box.Create() -- Bottom screen.
    , UI.Box.Create() -- Left screen.
    , UI.Box.Create() -- Right screen.
};
box.Phone.Overlay.Instance = UI.Box.Create();
-- New HUD bars have special place order.
box.Health.Border.Instance = UI.Box.Create();
box.Health.Border.Instance:Set( box.Health.Border.Config );
box.Health.Background.Instance = UI.Box.Create();
box.Health.Background.Instance:Set( box.Health.Background.Config );
box.Health.Current.Instance = UI.Box.Create();
box.Health.Current.Instance:Set( box.Health.Current.Config );
box.Health.Limit.Instance = UI.Box.Create();
box.Health.Limit.Instance:Set( box.Health.Limit.Config );
box.Stamina.Border.Instance = UI.Box.Create();
box.Stamina.Border.Instance:Set( box.Stamina.Border.Config );
box.Stamina.Background.Instance = UI.Box.Create();
box.Stamina.Background.Instance:Set( box.Stamina.Background.Config );
box.Stamina.Current.Instance = UI.Box.Create();
box.Stamina.Current.Instance:Set( box.Stamina.Current.Config );
box.Cutscene.Instances = 
{
    UI.Box.Create() -- Top box.
    , UI.Box.Create() -- Bottom box.
};
box.Cutscene.Config.nextHideTime = nil;
box.Cutscene.Config.yTop = -box.Cutscene.Config.height;
box.Cutscene.Config.yBottom = size.height;
for id,cmp in pairs(box.Cutscene.Instances)
do
    cmp:Set( box.Cutscene.Config );
    cmp:Set( { y = (id == 1 and box.Cutscene.Config.yTop or box.Cutscene.Config.yBottom) } );
end
box.Hint.Instance = UI.Box.Create();
box.Hint.Config.nextHideTime = nil;
box.Hint.Config.x = center.x - box.Hint.Config.width / 2;
box.Hint.Config.y = -box.Hint.Config.height;
box.Hint.Instance:Set( box.Hint.Config );


-- Init text(s).
text.Chapter.Instance = UI.Text.Create();
text.Chapter.Config.a = 255;
text.Chapter.Config.nextHideTime = nil;
text.Chapter.Instance:Set( text.Chapter.Config );
text.Phone.Subject.Instance = UI.Text.Create();
text.Phone.Message.Instance = UI.Text.Create();
text.Phone.Options.Instance = UI.Text.Create();
text.Phone.Back.Instance = UI.Text.Create();
text.Tooltip.Phone.Instance = UI.Text.Create();
text.Tooltip.Phone.Config.text = text.Tooltip.Phone.Config.Default.text;
text.Tooltip.Objective.Instance = UI.Text.Create();
text.Tooltip.Objective.Instance:Hide();
text.Icon.Health.Instance = UI.Text.Create();
text.Icon.Health.Instance:Set( text.Icon.Health.Config );
text.Icon.Stamina.Instance = UI.Text.Create();
text.Icon.Stamina.Instance:Set( text.Icon.Stamina.Config );
text.Phone.NewMessage.Instance = UI.Text.Create();
if text.Phone.NewMessage.ENABLED
then
    text.Phone.NewMessage.Config.nextHideTime = nil;
    text.Phone.NewMessage.Instance:Hide();
    text.Phone.NewMessage.Instance:Set( text.Phone.NewMessage.Config );
end
text.Subtitle.Instance = UI.Text.Create();
text.Subtitle.Config.a = 0;
text.Hint.Instance = UI.Text.Create();
text.Hint.Config.text = nil;
text.Hint.Config.x = center.x - text.Hint.Config.width / 2;
text.Hint.Config.y = box.Hint.Config.y + text.Hint.Config.height / 2;
text.Hint.Config.yMax = text.Hint.Config.height / 2;
text.Hint.Instance:Set( text.Hint.Config );
text.Hint.Config.Queue = { Text = {} , Line = 1 };
text.Prologue.Config.line = 0;
text.Prologue.Config.a = 0;
text.Prologue.Instances = {}
if text.Epilogue.Config.text == nil then text.Epilogue.Config.text = ''; end
text.Epilogue.Config.line = 0;
text.Epilogue.Config.aDrawed = {}
text.Epilogue.Config.yDrawed = {}
text.Epilogue.Instances = {}
if text.Credit.Config.text == nil then text.Credit.Config.text = '#THANKS FOR PLAYING!'; end
text.Credit.Config.line = 0;
text.Credit.Config.aDrawed = {}
text.Credit.Config.yDrawed = {}
text.Credit.Instances = {}


-- Checks whether screen fade is active.
function moduleScreenFade:IsOn()
    return box.ScreenFade.Config.a > 0;
end


-- Checks whether cutscene mode is active.
function moduleCutscene:IsOn()
    return isCutscene;
end


-- Checks whether phone is available.
function modulePhone:IsAvailable()
    return isPhoneAvailable;
end


-- Checks whether phone is available.
function modulePhone:IsVisible()
    return isPhoneVisible;
end


-- Checks whether prologue scene is active.
function modulePrologue:IsOn()
    return isPrologue;
end


-- Checks whether epilogue scene is active.
function moduleEpilogue:IsOn()
    return isEpilogue;
end


-- Checks whether credits scene is active.
function moduleCredit:IsOn()
    return isCredit;
end


-- Gets player health.
function moduleHealth:GetCurrent()
    return syncvalue.Health.Instance.value;
end


-- Gets player maximum health.
function moduleHealth:GetMax()
    return syncvalue.MaxHealth.Instance.value;
end


-- Gets player stamina.
function moduleStamina:GetCurrent()
    return syncvalue.Stamina.Instance.value;
end


-- Gets player maximum stamina.
function moduleStamina:GetMax()
    return COF.STAMINA_MAX;
end


-- Draws screen fade to player.
function moduleScreenFade:Draw( fademode , faderate, r , g , b , a )
    debug:print( "UI.ScreenFade:Draw()" );

    local config = box.ScreenFade.Config;
    local default = config.DEFAULT;
    fademode = tonumber( fademode , 10 ); if fademode == nil then fademode = default.fademode; end
    faderate = tonumber( faderate ); if faderate == nil then faderate = default.faderate; end
    r = tonumber( r ); if r == nil then r = default.r; end
    g = tonumber( g ); if g == nil then g = default.g; end
    b = tonumber( b ); if b == nil then b = default.b; end
    a = tonumber( a ); if a == nil then a = default.a; end

    if r > 255 then r = 255
    elseif r < 0 then r = 0 end;

    if g > 255 then g = 255
    elseif g < 0 then g = 0 end;

    if b > 255 then b = 255
    elseif b < 0 then b = 0 end;

    if a > 255 then a = 255
    elseif a < 0 then a = 0 end;

    debug:print( "fademode= " .. fademode );
    debug:print( "faderate= " .. faderate );
    debug:print( "r= " .. r );
    debug:print( "g= " .. g );
    debug:print( "b= " .. b );
    debug:print( "a= " .. a );

    config.fademode = fademode;
    config.faderate = faderate;
    config.r = r;
    config.g = g;
    config.b = b;
    config.a = a;

    if fademode == enum.FADEMODE.SET
    then
        config.aDrawed = a;
        config.rDrawed = config.r;
        config.gDrawed = config.g;
        config.bDrawed = config.b;

        box.ScreenFade.Instance:Set( config );
        box.ScreenFade.Instance:Set( { a = config.aDrawed } );
    end
end


-- Sets chapter title.
function moduleChapter:Set( part , title )
    debug:print( "UI.Chapter:Set()" );

    moduleChapter.Part , moduleChapter.Title = tostring(part) , tostring(title);
    debug:print( "part= " .. moduleChapter.Part );
    debug:print( "title= " .. moduleChapter.Title );

    text.Chapter.Config.text = string.format( text.Chapter.Config.format , moduleChapter.Part , moduleChapter.Title );
    text.Chapter.Instance:Set( { text = text.Chapter.Config.text } );

    box.Chapter.Config.width = 0;
    box.Chapter.Config.a = 0;
    text.Chapter.Config.a = 0;
    text.Chapter.Config.nextHideTime = UI:GetTime() + text.Chapter.Config.holdtime;
end


-- Sets cutscene mode.
function moduleCutscene:Set( isOn )
    debug:print( "UI.Cutscene:Set()" );

    isOn = ( isOn and true or false );
    debug:print( "isOn= " .. tostring(isOn) );

    -- Ignores same value.
    --if isOn == isCutscene then debug:print( "Same value." ); return; end

    isCutscene = isOn;

    debug:print( "Sends " .. (isOn and "ON" or "OFF") .. " signal." );
    UI.Signal( (isOn and enum.SIGNAL.CUTSCENE_ON or enum.SIGNAL.CUTSCENE_OFF) );
    moduleEvent:OnCutscene( isOn );

    if isOn
    then
        text.Icon.Health.Instance:Hide();
        text.Icon.Stamina.Instance:Hide();
        text.Tooltip.Phone.Instance:Hide();
        text.Phone.NewMessage.Instance:Hide();
        text.Tooltip.Objective.Instance:Hide();

        -- Sanity check.
        if lastPhoneVisibilityOnCutscene == nil
        then
            lastPhoneVisibilityOnCutscene = isPhoneVisible;
        end
        modulePhone:SetVisibility( false );

        box.Cutscene.Config.yTop = -box.Cutscene.Config.height;
        box.Cutscene.Config.yBottom = size.height;
    else
        text.Tooltip.Phone.Instance:Show();
        text.Icon.Health.Instance:Show();
        text.Icon.Stamina.Instance:Show();

        if lastPhoneVisibilityOnCutscene ~= nil
        then
            modulePhone:SetVisibility( lastPhoneVisibilityOnCutscene );
            lastPhoneVisibilityOnCutscene = nil;
        end

        if moduleObjective.Description ~= nil
        then
            text.Tooltip.Objective.Instance:Show();
        end

        box.Cutscene.Config.yTop = 0;
        box.Cutscene.Config.yBottom = size.height - box.Cutscene.Config.height;
    end

    -- Resets cutscene boxes.
    for id,cmp in pairs(box.Cutscene.Instances)
    do
        cmp:Set( { y = (id == 1 and box.Cutscene.Config.yTop or box.Cutscene.Config.yBottom) } );
    end

    UI.StopPlayerControl( isOn );
end


-- Sets phone availability.
function modulePhone:SetAvailability( isAvailable )
    debug:print( "UI.Phone:SetAvailability()" );

    isAvailable = ( isAvailable and true or false );
    debug:print( "isAvailable= " .. tostring(isAvailable) );

    if not isAvailable
    then
        text.Tooltip.Phone.Config.text = text.Tooltip.Phone.Config.NotAvailable.text;
    elseif isPhoneAvailable ~= isAvailable
    then
        text.Tooltip.Phone.Config.text = text.Tooltip.Phone.Config.Default.text;
    end
    isPhoneAvailable = isAvailable;
    text.Tooltip.Phone.Instance:Set( { text = text.Tooltip.Phone.Config.text } );
end


-- Sets phone screen visibility.
function modulePhone:SetVisibility( isVisible )
    debug:print( "UI.Phone:SetVisibility()" );

    if isVisible and not isPhoneAvailable
    then
        debug:print( "Phone not available." );
        return;
    end

    isVisible = ( isVisible and true or false );
    debug:print( "isVisible= " .. tostring(isVisible) );

    -- Ignores same value.
    --if isVisible == isPhoneVisible then debug:print( "Same value." ); return; end

    isPhoneVisible = isVisible;

    local components = 
    {
        box.Phone.Screen.Instance
        , box.Phone.Overlay.Instance
        , text.Phone.Subject.Instance
        , text.Phone.Message.Instance
        , text.Phone.Options.Instance
        , text.Phone.Back.Instance
    };
    for _ , border in pairs( box.Phone.Border.Instances )
    do
        table.insert( components , border );
    end

    for _ , component in pairs( components )
    do
        if not isPhoneVisible
        then
            component:Hide();
        else
            component:Show();
            
            text.Tooltip.Phone.Config.text = text.Tooltip.Phone.Config.Default.text;
            text.Tooltip.Phone.Instance:Set( { text = text.Tooltip.Phone.Config.text } );
            box.Phone.Overlay.Config.a = 255;
            box.Phone.Overlay.Instance:Set( { a = box.Phone.Overlay.Config.a } );
        end
    end
end


-- Sets phone message.
function modulePhone:SetMessage( msgID )
    debug:print( "UI.Phone:SetMessage()" );
    debug:print( "msgID= " .. tostring(msgID) );

    if msgID == nil then return; end
    local lastMsgID = text.Phone.Message.Config.lastMsgID;
    debug:print( "lastID= " .. tostring(lastMsgID) );

    -- Ignores same value.
    if msgID == lastMsgID then debug:print( "Same value." ); return; end

    text.Phone.Message.Config.lastMsgID = msgID;
    local msgConfig = COF.PhoneMsg[ msgID ];
    local subject = "INVALID_MESSAGE_ID";
    local msg = "INVALID_MESSAGE_ID";

    -- Valid message table.
    if msgConfig ~= nil
    then
        local wrap = msgConfig.EnableTextWrap;
        if wrap == nil then wrap = COF.PhoneMsg.DEFAULT.EnableTextWrap; end
        subject = msgConfig.Subject;
        if subject == nil
        then
            subject = "SUBJECT_IS_MISSING";
            debug:print( "subject= nil" );
        end

        msg = msgConfig.Message;
        if msg == nil
        then
            msg = "MESSAGE_IS_MISSING";
            debug:print( "msg= nil" );
        elseif wrap
        then
            local totalChars = 0;
            local totalLines = 1;
            local charWidth, charHeight = text:GetSize( text.Phone.Message.Config.font );
            local maxCharsPerLine = math.floor( text.Phone.Message.Config.width / charWidth );
            local maxLines = math.floor( text.Phone.Message.Config.height / charHeight );
            local words = string.explode( msg , ' ' );
            msg = "";
            for i = 1, #words
            do
                local word = words[i];
                local len = string.len( word );
                local sumLen = totalChars + len;

                if sumLen <= maxCharsPerLine
                then
                    totalChars = sumLen;
                    msg = msg .. word;

                    if totalChars ~= maxCharsPerLine
                    then
                        totalChars = totalChars + 1;
                        msg = msg .. " ";
                    else
                        totalChars = 0;

                        totalLines = totalLines + 1;
                        if totalLines > maxLines then break; end
                        msg = msg .. "\n";
                    end
                else
                    totalChars = len;
                    if i ~= 1
                    then
                        totalLines = totalLines + 1;
                        if totalLines > maxLines then break; end
                        msg = msg .. "\n";
                    end

                    msg = msg .. word;

                    if totalChars ~= maxCharsPerLine
                    then
                        totalChars = totalChars + 1;
                        msg = msg .. " ";
                    end
                end
            end

            debug:print( "len= " .. string.len(msg) );
            debug:print( "chars/line= " .. maxCharsPerLine );
            debug:print( "lines= " .. totalLines );
            debug:print( "words= " .. #words );
        end
    else
        debug:print( "Config= nil" );
    end

    box.Phone.Overlay.Config.a = 255;
    box.Phone.Overlay.Instance:Set( { a = box.Phone.Overlay.Config.a } );
    text.Phone.Subject.Config.text = subject;
    text.Phone.Message.Config.text = msg;
    text.Phone.Subject.Instance:Set( { text = subject } );
    text.Phone.Message.Instance:Set( { text = msg } );

    if not isPhoneVisible and isPhoneAvailable and msgID ~= "DEFAULT"
    then
        text.Tooltip.Phone.Config.text = text.Tooltip.Phone.Config.New.text;
        text.Tooltip.Phone.Instance:Set( { text = text.Tooltip.Phone.Config.text } );
    end

    if text.Phone.NewMessage.ENABLED and msgID ~= "DEFAULT"
    then
        text.Phone.NewMessage.Config.nextHideTime = 0;
        text.Phone.NewMessage.Config.printed = nil;
    end
end


-- Sets subtitle text.
function moduleSubtitle:Set( subID )
    debug:print( "UI.Subtitle:Set()" );
    debug:print( "subID= " .. tostring(subID) );

    if subID == nil then return; end

    local subConfig = COF.Subtitle[ subID ];
    local subTxt = subID; --"INVALID_SUBTITLE_ID";
    local len = string.len( subTxt );

    -- Valid message table.
    if subConfig ~= nil
    then
        subTxt = subConfig.text;
        if subTxt == nil
        then
            subTxt = "TEXT_IS_MISSING";
            debug:print( "text= nil" );
        else
            -- Gets longest string length.
            len = 0;
            for _ , word in pairs( string.explode( subTxt , '\n' ) )
            do
                local wordLen = string.len( word );
                if wordLen > len
                then
                    len = wordLen;
                end
            end
        end
    else
        debug:print( "Config= nil" );
    end

    debug:print( "len= " .. tostring(len) );

    local config = text.Subtitle.Config;
    -- Fade time.
    config.fadetime = config.faderate / 255;
    -- Next hide time.
    config.nextHideTime = UI:GetTime() + ( ( config.holdtime ~= nil and config.holdtime or COF.Subtitle.DEFAULT.holdtime ) - config.fadetime * 2 );
    -- Automatically adjust font width.
    local charWidth = text:GetSize( config.font );
    config.width = charWidth * len;
    debug:print( "width= " .. tostring(config.width) );
    -- Automatically adjust font position.
    config.x = center.x - config.width / 2;
    config.y = size.height - COF.UI.Box.Cutscene.Config.height - COF.HUD_PADDING - config.height;
    -- Assign config color.
    config.r = ( subConfig ~= nil and subConfig.r ~= nil and subConfig.r or COF.Subtitle.DEFAULT.r );
    config.g = ( subConfig ~= nil and subConfig.g ~= nil and subConfig.g or COF.Subtitle.DEFAULT.g );
    config.b = ( subConfig ~= nil and subConfig.b ~= nil and subConfig.b or COF.Subtitle.DEFAULT.b );
    -- Reset subtitle alpha.
    config.a = 0;
    config.text = subTxt;
    text.Subtitle.Instance:Set( config );
end


-- Resets the hint box.
function moduleHint:Reset()
    text.Hint.Config.Queue = { Text = {} , Line = 1 };
    text.Hint.Config.text = nil;
    box.Hint.Config.nextHideTime = nil;

    box.Hint.Config.y = -box.Hint.Config.height;
    box.Hint.Instance:Set( { y = box.Hint.Config.y } );

    text.Hint.Config.y = box.Hint.Config.y + text.Hint.Config.height / 2;
    text.Hint.Instance:Set( { y = text.Hint.Config.y } );
end


-- Enqueues the text into the hint queue list.
function moduleHint:Enqueue( txt )
    debug:print( "UI.Hint:Enqueue()" );
    debug:print( "txt= " .. tostring( txt ) );

    if txt == nil then return; end

    table.insert( text.Hint.Config.Queue.Text , txt );
end


-- Dequeues first element from hint queue list.
function moduleHint:Dequeue()
    debug:print( "UI.Hint:Dequeue()" );

    local total = #text.Hint.Config.Queue.Text;
    debug:print( "total= " .. total );
    if total > 0
    then
        debug:print( "text= " .. tostring( table.remove( text.Hint.Config.Queue.Text , 1 ) ) );
    end

    text.Hint.Config.text = nil;
    text.Hint.Config.Queue.Line = 1;
    box.Hint.Config.nextHideTime = nil;
end


-- Sets the objective description.
function moduleObjective:Set( desc )
    debug:print( "UI.Objective:Set()" );
    debug:print( "desc= " .. tostring( desc ) );

    if desc == self.Description then debug:print( "Same value." ); return; end
    if string.len( desc ) <= 0 then desc = nil; end

    self.Description = desc;

    if desc == nil
    then
        text.Tooltip.Objective.Instance:Hide();
    else
        if not isCutscene
        then
            text.Tooltip.Objective.Instance:Show();
        end
        moduleHint:Enqueue( desc );
    end
end


-- Shown the objective on hint box.
function moduleObjective:Show()
    debug:print( "UI.Objective:Show()" );

    local desc = self.Description;
    debug:print( "desc= " .. tostring( desc ) );

    if desc == nil or string.len( desc ) <= 0 then return; end

    moduleHint:Reset();
    moduleHint:Enqueue( desc );
end


-- Shown prologue scene.
function modulePrologue:Show( isOn )
    debug:print( "UI.Prologue:Show()" );

    isOn = ( isOn and true or false );
    debug:print( "isOn= " .. tostring(isOn) );

    isPrologue = isOn;

    local config = text.Prologue.Config;

    if isOn
    then
        if config.text == nil or string.len( config.text ) < 1
        then
            isPrologue = false;
            debug:print( "Text is empty." );
        
        else
            local qty = #text.Prologue.Instances;
            local lines = string.explode( config.text , '\n' );
            config.line = #lines;

            debug:print( "qty= " .. tostring(qty) );

            if config.line > qty
            then
                for i = qty + 1, config.line
                do
                    text.Prologue.Instances[i] = UI.Text.Create();
                    text.Prologue.Instances[i]:Set( config );
                end
            end

            qty = #text.Prologue.Instances;

            debug:print( "qty= " .. tostring(qty) );
            debug:print( "lines= " .. tostring(config.line) );

            local y = center.y - (config.height * config.line) / 2;
            local charWidth = text:GetSize( config.font );
            for i = 1, config.line
            do
                if i > 1 then y = y + config.height; end

                local txt = lines[i];
                local width = string.len( txt ) * charWidth;

                text.Prologue.Instances[i]:Set( { text = txt, width = width, x = center.x - width / 2, y = y } );
            end

            config.a = 0;
            for i = 1, qty
            do
                text.Prologue.Instances[i]:Set( { a = config.a } );
            end
        end
    end

    debug:print( "Sends " .. (isPrologue and "ON" or "OFF") .. " signal." );
    UI.Signal( (isPrologue and enum.SIGNAL.PROLOGUE_ON or enum.SIGNAL.PROLOGUE_OFF) );
    moduleEvent:OnPrologue( isPrologue );
end


-- Shown epilogue scene.
function moduleEpilogue:Show( txt )
    debug:print( "UI.Epilogue:Show()" );

    if txt == nil
    then
        isEpilogue = false;
    else
        txt = tostring(txt);
        isEpilogue = string.len( txt ) > 0;
    end

    debug:print( "txt= " .. tostring(txt) );

    local config = text.Epilogue.Config;
    config.text = txt;

    local qty = #text.Epilogue.Instances;
    debug:print( "qty= " .. tostring(qty) );

    if isEpilogue
    then
        doEpilogueSendSignalOff = true;

        local lines = string.explode( config.text , '\n' );
        config.line = #lines;

        if config.line > qty
        then
            for i = qty + 1, config.line
            do
                text.Epilogue.Instances[i] = UI.Text.Create();
                text.Epilogue.Instances[i]:Set( config );
            end
        end

        qty = #text.Epilogue.Instances;

        debug:print( "qty= " .. tostring(qty) );
        debug:print( "lines= " .. tostring(config.line) );

        local y = config.yBottom;
        local font = config.font;
        local charWidth = text:GetSize( font );
        local charHeight = config.height;
        for i = 1, config.line
        do
            local lineText = lines[i];
            font = config.font;
            -- Header text.
            if string.find( lineText,'#' ) == 1
            then
                font = config.fontHeader;
                charWidth = text:GetSize( font );
                charHeight = config.heightHeader;
                lineText = string.sub( lineText, 2 );
            end
            
            if i > 1 then y = y + charHeight; end

            local width = string.len( lineText ) * charWidth;
            config.yDrawed[i] = y;

            text.Epilogue.Instances[i]:Set( { text = lineText, width = width, x = center.x - width / 2, y = y, font = font, height = charHeight } );
        end
    end

    for i = 1, qty
    do
        if isEpilogue and i == 1 
        then 
            config.aDrawed[i] = 255;
        else
            config.aDrawed[i] = 0;
        end

        text.Epilogue.Instances[i]:Set( { a = config.aDrawed[i] } );
    end

    debug:print( "Sends " .. (isEpilogue and "ON" or "OFF") .. " signal." );
    UI.Signal( (isEpilogue and enum.SIGNAL.EPILOGUE_ON or enum.SIGNAL.EPILOGUE_OFF) );
    moduleEvent:OnEpilogue( isEpilogue );
end


-- Shown credits scene.
function moduleCredit:Show( isOn )
    debug:print( "UI.Credit:Show()" );

    isOn = ( isOn and true or false );
    debug:print( "isOn= " .. tostring(isOn) );

    isCredit = isOn;

    local config = text.Credit.Config;
    local qty = #text.Credit.Instances;

    if isOn
    then
        if config.text == nil or string.len( config.text ) < 1
        then
            isCredit = false;
            debug:print( "Text is empty." );

        else
            doCreditSendSignalOff = true;

            local lines = string.explode( config.text , '\n' );
            config.line = #lines;

            if config.line > qty
            then
                for i = qty + 1, config.line
                do
                    text.Credit.Instances[i] = UI.Text.Create();
                    text.Credit.Instances[i]:Set( config );
                end
            end

            qty = #text.Credit.Instances;

            debug:print( "qty= " .. tostring(qty) );
            debug:print( "lines= " .. tostring(config.line) );

            local y = config.yTop - config.heightHeader;
            local font = config.font;
            local charWidth = text:GetSize( font );
            local charHeight = config.height;
            for i = 1, config.line
            do
                local lineText = lines[i];
                font = config.font;
                -- Header text.
                if string.find( lineText,'#' ) == 1
                then
                    font = config.fontHeader;
                    charWidth = text:GetSize( font );
                    charHeight = config.heightHeader;
                    lineText = string.sub( lineText, 2 );
                end
                
                if i > 1 then y = y - charHeight; end

                local width = string.len( lineText ) * charWidth;
                config.yDrawed[i] = y;

                text.Credit.Instances[i]:Set( { text = lineText, width = width, x = center.x - width / 2, y = y, font = font, height = charHeight } );
            end
        end
    end

    for i = 1, qty
    do
        config.aDrawed[i] = 0;
        text.Credit.Instances[i]:Set( { a = config.aDrawed[i] } );
    end

    debug:print( "Sends " .. (isCredit and "ON" or "OFF") .. " signal." );
    UI.Signal( (isCredit and enum.SIGNAL.CREDIT_ON or enum.SIGNAL.CREDIT_OFF) );
    moduleEvent:OnCredit( isCredit );
end


-- Updates phone screen components.
function modulePhone:UpdateScreen()
    -- Phone screen frame.
    box.Phone.Screen.Config.x = size.width - COF.HUD_PADDING - box.Phone.Screen.Config.width - box.Phone.Border.Config.thickness * 2;
    box.Phone.Screen.Config.y = size.height - COF.HUD_PADDING - COF.HUD_ICON_CROSS_HEIGHT - COF.HUD_PADDING - COF.HUD_ICON_NUMERIC_HEIGHT - 25 - box.Phone.Screen.Config.height - box.Phone.Border.Config.thickness * 2;
    box.Phone.Screen.Instance:Set( box.Phone.Screen.Config );
    box.Phone.Border.Config.width = box.Phone.Screen.Config.width;
    box.Phone.Border.Config.height = box.Phone.Border.Config.thickness;
    box.Phone.Border.Config.x = box.Phone.Screen.Config.x;
    box.Phone.Border.Config.y = box.Phone.Screen.Config.y - box.Phone.Border.Config.thickness + 2;
    box.Phone.Border.Instances[3]:Set( box.Phone.Border.Config );
    box.Phone.Border.Config.x = box.Phone.Screen.Config.x;
    box.Phone.Border.Config.y = box.Phone.Screen.Config.y + box.Phone.Screen.Config.height - 2;
    box.Phone.Border.Instances[4]:Set( box.Phone.Border.Config );
    box.Phone.Border.Config.width = box.Phone.Border.Config.thickness;
    box.Phone.Border.Config.height = box.Phone.Screen.Config.height;
    box.Phone.Border.Config.x = box.Phone.Screen.Config.x - box.Phone.Border.Config.thickness + 2;
    box.Phone.Border.Config.y = box.Phone.Screen.Config.y;
    box.Phone.Border.Instances[5]:Set( box.Phone.Border.Config );
    box.Phone.Border.Config.x = box.Phone.Screen.Config.x + box.Phone.Screen.Config.width - 2;
    box.Phone.Border.Config.y = box.Phone.Screen.Config.y;
    box.Phone.Border.Instances[6]:Set( box.Phone.Border.Config );

    -- Phone screen containers.
    box.Phone.Border.Config.width = box.Phone.Screen.Config.width;
    box.Phone.Border.Config.height = box.Phone.Border.Config.thickness;
    box.Phone.Border.Config.x = box.Phone.Screen.Config.x;
    text.Phone.Subject.Config.x = box.Phone.Screen.Config.x + box.Phone.Screen.Config.padding;
    text.Phone.Subject.Config.y = box.Phone.Screen.Config.y + box.Phone.Screen.Config.padding + box.Phone.Screen.Config.height * 0.05;
    text.Phone.Subject.Config.width = box.Phone.Screen.Config.width - box.Phone.Screen.Config.padding * 2;
    text.Phone.Subject.Config.height = box.Phone.Screen.Config.height * 0.15 - box.Phone.Screen.Config.padding;
    text.Phone.Subject.Instance:Set( text.Phone.Subject.Config );
    box.Phone.Border.Config.y = text.Phone.Subject.Config.y + text.Phone.Subject.Config.height - box.Phone.Border.Config.thickness - box.Phone.Screen.Config.padding;
    box.Phone.Border.Instances[1]:Set( box.Phone.Border.Config );
    text.Phone.Message.Config.x = box.Phone.Screen.Config.x + box.Phone.Screen.Config.padding;
    text.Phone.Message.Config.y = box.Phone.Border.Config.y + box.Phone.Border.Config.thickness + box.Phone.Screen.Config.padding + box.Phone.Screen.Config.height * 0.05;
    text.Phone.Message.Config.width = box.Phone.Screen.Config.width - box.Phone.Screen.Config.padding * 2;
    text.Phone.Message.Config.height = box.Phone.Screen.Config.height * 0.6 - box.Phone.Screen.Config.padding;
    text.Phone.Message.Instance:Set( text.Phone.Message.Config );
    box.Phone.Border.Config.y = text.Phone.Message.Config.y + text.Phone.Message.Config.height - box.Phone.Border.Config.thickness - box.Phone.Screen.Config.padding;
    box.Phone.Border.Instances[2]:Set( box.Phone.Border.Config );
    text.Phone.Options.Config.height = box.Phone.Screen.Config.height * 0.15 - box.Phone.Screen.Config.padding;
    text.Phone.Options.Config.y = box.Phone.Border.Config.y + box.Phone.Border.Config.thickness + box.Phone.Screen.Config.padding + box.Phone.Screen.Config.height * 0.05;
    text.Phone.Options.Config.x = box.Phone.Screen.Config.x + box.Phone.Screen.Config.padding;
    text.Phone.Options.Instance:Set( text.Phone.Options.Config );
    text.Phone.Back.Config.height = text.Phone.Options.Config.height;
    text.Phone.Back.Config.y = text.Phone.Options.Config.y;
    local charWidth = text:GetSize( text.Phone.Back.Config.font );
    text.Phone.Back.Config.x = size.width - COF.HUD_PADDING - box.Phone.Border.Config.thickness - box.Phone.Screen.Config.padding - text.Phone.Back.Config.width - charWidth;
    text.Phone.Back.Instance:Set( text.Phone.Back.Config );

    -- Phone screen on/off/flash overlay.
    box.Phone.Overlay.Config.width  = box.Phone.Screen.Config.width;
    box.Phone.Overlay.Config.height = box.Phone.Screen.Config.height;
    box.Phone.Overlay.Config.x = box.Phone.Screen.Config.x;
    box.Phone.Overlay.Config.y = box.Phone.Screen.Config.y;
    box.Phone.Overlay.Config.a = ( isPhoneAvailable and 0 or 255 );
    box.Phone.Overlay.Instance:Set( box.Phone.Overlay.Config );

    -- Phone tooltip.
    text.Tooltip.Phone.Config.x = size.width - COF.HUD_PADDING - text.Tooltip.Phone.Config.width;
    text.Tooltip.Phone.Config.y = box.Phone.Screen.Config.y - text.Tooltip.Phone.Config.height - box.Phone.Border.Config.thickness;
    text.Tooltip.Phone.Instance:Set( text.Tooltip.Phone.Config );

    -- Objective tooltip.
    text.Tooltip.Objective.Config.x = text.Tooltip.Phone.Config.x;
    text.Tooltip.Objective.Config.y = text.Tooltip.Phone.Config.y - text.Tooltip.Objective.Config.height;
    text.Tooltip.Objective.Instance:Set( text.Tooltip.Objective.Config );

    modulePhone:SetVisibility( isPhoneVisible );
    modulePhone:SetAvailability( isPhoneAvailable );
end
modulePhone:UpdateScreen();
modulePhone:SetMessage( "DEFAULT" );


-- Update ScreenFade components.
function moduleScreenFade:Update()
    local config = box.ScreenFade.Config;
    local alpha = config.aDrawed;
    local red, green, blue = config.rDrawed, config.gDrawed, config.bDrawed;
    local mode = config.fademode;
    local limit = config.a; if mode == enum.FADEMODE.OUT then limit = 0; end

    if mode == enum.FADEMODE.SET
        or
        ( alpha == limit
        and red == config.r
        and green == config.g
        and blue == config.b ) then return; end

    local rate = config.faderate;

    if mode == enum.FADEMODE.IN
    then
        alpha = alpha + rate;
        if alpha > limit
        then
            alpha = limit;
        end
    elseif mode == enum.FADEMODE.OUT
    then
        alpha = alpha - rate;
        if alpha < limit
        then
            alpha = limit;
        end
    end

    -- Adjust red color.
    limit = config.r;
    if red < limit
    then
        red = red + rate;
        if red > limit
        then
            red = limit;
        end
    elseif red > limit
    then
        red = red - rate;
        if red < limit
        then
            red = limit;
        end
    end

    -- Adjust green color.
    limit = config.g;
    if green < limit
    then
        green = green + rate;
        if green > limit
        then
            green = limit;
        end
    elseif green > limit
    then
        green = green - rate;
        if green < limit
        then
            green = limit;
        end
    end

    -- Adjust blue color.
    limit = config.b;
    if blue < limit
    then
        blue = blue + rate;
        if blue > limit
        then
            blue = limit;
        end
    elseif blue > limit
    then
        blue = blue - rate;
        if blue < limit
        then
            blue = limit;
        end
    end

    config.aDrawed = alpha;
    config.rDrawed, config.gDrawed, config.bDrawed = red, green, blue;
    box.ScreenFade.Instance:Set( { a = alpha , r = red , g = green , b = blue } );
end


-- Updates chapter components.
function moduleChapter:Update()
    local configTxt = text.Chapter.Config;
    local nextHideTime = configTxt.nextHideTime;

    if nextHideTime == nil then return; end
    local time = UI:GetTime();
    local alphaTxt = configTxt.a;
    local rateTxt = configTxt.faderate;
    local configBox = box.Chapter.Config;
    local alphaBox = configBox.a;
    local rateBox = configBox.faderate;
    local width = configBox.width;
    local maxWidth = configTxt.width;
    local rateSlide = configBox.slideuprate;

    if time < nextHideTime
    then
        alphaTxt = alphaTxt + rateTxt;
        alphaBox = alphaBox + rateBox;
        width = width + rateSlide;
    else
        alphaTxt = alphaTxt - rateTxt;
        alphaBox = alphaBox - rateBox;

        if alphaTxt <= 0 and alphaBox <= 0
        then
            configTxt.nextHideTime = nil;
        end
    end

    if alphaTxt > 255
    then
        alphaTxt = 255;
    elseif alphaTxt < 0
    then
        alphaTxt = 0;
    end

    if alphaBox > 255
    then
        alphaBox = 255;
    elseif alphaBox < 0
    then
        alphaBox = 0;
    end

    if width < 0
    then
        width = 0;
    elseif width > maxWidth
    then
        width = maxWidth;
    end

    configTxt.a = alphaTxt;
    text.Chapter.Instance:Set( { a = alphaTxt } );

    configBox.a = alphaBox;
    configBox.width = width;
    box.Chapter.Instance:Set( { a = alphaBox , width = width } );
end


-- Updates cutscene components.
function moduleCutscene:Update()
    local config = box.Cutscene.Config;
    local rate = config.sliderate;
    local yTop = config.yTop;
    local yBottom = config.yBottom;
    local yTopFinal = 0;
    local yBottomFinal = size.height - box.Cutscene.Config.height;

    if isCutscene
    then
        -- Top box.
        if yTop < yTopFinal
        then
            yTop = yTop + rate;

        elseif yTop > yTopFinal
        then
            yTop = yTopFinal;
        end

        -- Bottom box.
        if yBottom > yBottomFinal
        then
            yBottom = yBottom - rate;

        elseif yBottom < yBottomFinal
        then
            yBottom = yBottomFinal;
        end
    
    else
        yTopFinal = -box.Cutscene.Config.height;
        yBottomFinal = size.height;

        -- Top box.
        if yTop > yTopFinal
        then
            yTop = yTop - rate;

        elseif yTop < yTopFinal
        then
            yTop = yTopFinal;
        end

        -- Bottom box.
        if yBottom < yBottomFinal
        then
            yBottom = yBottom + rate;

        elseif yBottom > yBottomFinal
        then
            yBottom = yBottomFinal;
        end
    end

    config.yTop = yTop;
    config.yBottom = yBottom;
    for id,cmp in pairs(box.Cutscene.Instances)
    do
        cmp:Set( { y = (id == 1 and box.Cutscene.Config.yTop or box.Cutscene.Config.yBottom) } );
    end
end


-- Updates phone screen overlay components.
function modulePhone:UpdateOverlay()
    if not isPhoneVisible then return; end

    local config = box.Phone.Overlay.Config;
    local alpha = config.a;
    local rate = config.faderate;
    
    if isPhoneAvailable
    then
        alpha = alpha - rate;
    else
        alpha = alpha + rate;

        if alpha >= 255 and isPhoneVisible
        then
            modulePhone:SetVisibility( not isPhoneVisible );
        end
    end

    if alpha > 255
    then
        alpha = 255;
    elseif alpha < 0
    then
        alpha = 0;
    end

    config.a = alpha;
    box.Phone.Overlay.Instance:Set( { a = alpha } );
end


-- Updates phone 'new message is received' alert components.
function modulePhone:UpdateNewSMSAlert()
    if not text.Phone.NewMessage.ENABLED then return; end
    if isCutscene then return; end
    
    local time = UI:GetTime();
    local config = text.Phone.NewMessage.Config;
    local nextHideTime = config.nextHideTime;

    if nextHideTime == nil then return; end;

    -- Show time.
    if nextHideTime == 0
    then
        text.Phone.NewMessage.Instance:Show();

    -- Expired.
    elseif nextHideTime <= time
    then
        config.nextHideTime = nil;
        text.Phone.NewMessage.Instance:Hide();
        return;
    end

    local printed = config.printed ~= nil and config.printed or "";
    local txt = config.text;
    local txtLen = string.len( txt );
    local printedLen = string.len( printed );
    
    if printedLen >= txtLen then return; end

    local index = printedLen + 1;
    printed = printed .. string.sub( txt, index, index );

    config.printed = printed;
    text.Phone.NewMessage.Instance:Set( { text = printed } );

    if string.len( printed ) == txtLen
    then
        config.nextHideTime = time + config.holdtime;
    end
end


-- Updates subtitle components.
function moduleSubtitle:Update()
    local config = text.Subtitle.Config;
    local nextHideTime = config.nextHideTime;

    if nextHideTime == nil then return; end

    local time = UI:GetTime();
    local alpha = config.a;
    local rate = config.faderate;

    if time < nextHideTime
    then
        alpha = alpha + rate;
    else
        alpha = alpha - rate;

        if alpha <= 0
        then
            config.nextHideTime = nil;
        end
    end
    
    if alpha > 255
    then
        alpha = 255;
    elseif alpha < 0
    then
        alpha = 0;
    end

    config.a = alpha;
    text.Subtitle.Instance:Set( { a = alpha } );
end


-- Updates hint components.
function moduleHint:Update()
    local configTxt = text.Hint.Config;
    local queue = configTxt.Queue;
    local queueText = queue.Text;

    if #queueText < 1 then return; end

    local time = UI:GetTime();
    local configBox = box.Hint.Config;
    local nextHideTime = configBox.nextHideTime;
    local lineID = queue.Line;
    local maxLine = 1;
    local txt = queueText[1];
    local yLimit = 0;

    if nextHideTime == nil
    then
        if configTxt.text == nil
        then
            local subConfig = COF.Hint[ txt ];
            if subConfig ~= nil
            then
                -- Invalid config.
                if subConfig.Lines == nil
                then
                    self:Dequeue();
                    return;
                end

                maxLine = #subConfig.Lines;
                -- Empty line.
                if maxLine < 1
                then
                    self:Dequeue();
                    return;
                end

                txt = subConfig.Lines[ lineID ];
            end

            -- Exceeding max lines.
            if lineID > maxLine
            then
                self:Dequeue();
                return;
            end
            
            configTxt.text = txt;
            text.Hint.Instance:Set( { text = txt } );
            queue.Line = lineID + 1;
        end

        configBox.y = configBox.y + configBox.sliderate;
        if configBox.y >= yLimit
        then
            configBox.y = yLimit;
            nextHideTime = time + configBox.holdtime;
        end
    
    elseif nextHideTime < time
    then
        yLimit = -box.Hint.Config.height;
        configBox.y = configBox.y - configBox.sliderate;
        if configBox.y <= yLimit
        then
            configBox.y = yLimit;
            nextHideTime = nil;
            configTxt.text = nil;
        end
    end

    configBox.nextHideTime = nextHideTime;
    configTxt.y = configBox.y + configTxt.height / 2;
    box.Hint.Instance:Set( { y = configBox.y } );
    text.Hint.Instance:Set( { y = configTxt.y } );
end


-- Updates health bar components.
function moduleHealth:Update()
    local box = box.Health;
    local max = moduleHealth:GetMax(); if max == nil or max < 0 then max = 0; end
    local hp  = moduleHealth:GetCurrent(); if hp == nil or hp < 0 then hp = 0; end
    local config = box.Limit.Config;

    -- Max. health.
    if max < COF.MAX_HEALTH_COMMON
    then
        config.width = -(1-max/COF.MAX_HEALTH_COMMON) * box.Background.Config.width;
        max = COF.MAX_HEALTH_COMMON;
    else
        config.width = 0;
    end
    -- Sets health limit bar.
    box.Limit.Instance:Set( { width = config.width });

    -- Sets health bar.
    config = box.Current.Config;
    if config.width == nil then config.width = 0; end
    local maxWidth = (hp/max) * box.Background.Config.width;
    local width = maxWidth;

    if maxWidth > config.width
    then
        width = config.width + config.slideuprate;
        if width > maxWidth then width = maxWidth; end
    end

    config.width = width;
    box.Current.Instance:Set( { width = config.width });
end


-- Updates stamina bar components.
function moduleStamina:Update()
    local box = box.Stamina;
    local max = moduleStamina:GetMax(); if max == nil or max < 0 then max = 0; end
    local stamina = moduleStamina:GetCurrent(); if stamina == nil or stamina < 0 then stamina = 0; end
    local config = box.Current.Config;

    -- Sets stamina bar.
    if config.width == nil then config.width = 0; end
    local maxWidth = (stamina/max) * box.Background.Config.width;
    local width = maxWidth;

    if maxWidth > config.width
    then
        width = config.width + config.slideuprate;
        if width > maxWidth then width = maxWidth; end
    end

    config.width = width;
    box.Current.Instance:Set( { width = config.width });

    -- Sudden stamina lost, flash it from red to normal.
    local red = config.r_value; if red == nil or red < 0 then red = config.r; end
    local green = config.g_value; if green == nil or green < 0 then green = config.g; end
    local blue = config.b_value; if blue == nil or blue < 0 then blue = config.b; end
    if lastStamina - stamina > (max * 0.1)
    then
        red = 255;
        green = 0;
        blue = 0;
    else
        if red > config.r
        then
            red = red - config.fadeoutrate;
            if red < config.r then red = config.r; end
        end

        if green < config.g
        then
            green = green + config.fadeoutrate;
            if green > config.g then green = config.g; end
        end

        if blue < config.b
        then
            blue = blue + config.fadeoutrate;
            if blue > config.b then blue = config.b; end
        end
    end
    
    config.r_value = red;
    config.g_value = green;
    config.b_value = blue;
    box.Current.Instance:Set( { r = config.r_value , g = config.g_value , b = config.b_value });

    lastStamina = stamina;
end


-- Update prologue components.
function modulePrologue:Update()
    local config = text.Prologue.Config;
    local alpha = config.a;
    local limit = ( isPrologue and 255 or 0 );

    if alpha == limit then return; end

    local rate = config.faderate;

    if isPrologue
    then
        alpha = alpha + rate;
        if alpha > limit
        then
            alpha = limit;
        end
    else
        alpha = alpha - rate;
        if alpha < limit
        then
            alpha = limit;
        end

        if alpha == limit
        then
            debug:print( "UI.Prologue:Update()" );
            debug:print( "Sends OFF signal." );
            UI.Signal( enum.SIGNAL.PROLOGUE_OFF );
            moduleEvent:OnPrologue( false );
        end
    end

    config.a = alpha;
    for i = 1, config.line
    do
        text.Prologue.Instances[i]:Set( { a = config.a } );
    end
end


-- Update epilogue components.
function moduleEpilogue:Update()
    if not isEpilogue then return; end

    local config = text.Epilogue.Config;
    local faderate = config.faderate;

    for i = 1, config.line
    do
        local instance = text.Epilogue.Instances[i];
        local alpha = config.aDrawed[i];
        local y = config.yDrawed[i];
        local doModify = false;

        if y > config.yTop
        then
            doModify = true;

            y = y - config.scrolluprate;
            if y < config.yBottom and alpha < config.a
            then
                alpha = alpha + faderate;
                if alpha > config.a
                then
                    alpha = config.a;
                end
            end
        elseif alpha > 0
        then
            doModify = true;

            alpha = alpha - faderate;
            if alpha < 0
            then
                alpha = 0;
            end

            if doEpilogueSendSignalOff and i == config.line and alpha == 0
            then
                doEpilogueSendSignalOff = false;
                debug:print( "UI.Epilogue:Update()" );
                debug:print( "Sends OFF signal." );
                UI.Signal( enum.SIGNAL.EPILOGUE_OFF );
                moduleEvent:OnEpilogue( false );
            end
        end

        if doModify
        then
            config.aDrawed[i] = alpha;
            config.yDrawed[i] = y;

            instance:Set( { y = y , a = alpha } );
        end
    end
end


-- Update credit components.
function moduleCredit:Update()
    if not isCredit then return; end

    local config = text.Credit.Config;
    local faderate = config.faderate;

    for i = 1, config.line
    do
        local instance = text.Credit.Instances[i];
        local alpha = config.aDrawed[i];
        local y = config.yDrawed[i];
        local doModify = false;
        local limit = config.yBottom;
        if i == config.line
        then
            limit = center.y;
        end

        if y < limit
        then
            doModify = true;

            y = y + config.scrolldownrate;
            if y > config.yTop and alpha < config.a
            then
                alpha = alpha + faderate;
                if alpha > config.a
                then
                    alpha = config.a;
                end
            end
        elseif i ~= config.line
        then
            if alpha > 0
            then
                doModify = true;

                alpha = alpha - faderate;
                if alpha < 0
                then
                    alpha = 0;
                end
            end
        else
            if doCreditSendSignalOff
            then
                doCreditSendSignalOff = false;
                debug:print( "UI.Credit:Update()" );
                debug:print( "Sends OFF signal." );
                UI.Signal( enum.SIGNAL.CREDIT_OFF );
                moduleEvent:OnCredit( false );
            end
        end

        if doModify
        then
            config.aDrawed[i] = alpha;
            config.yDrawed[i] = y;

            instance:Set( { y = y , a = alpha } );
        end
    end
end


-- SyncValue screenfade OnSync hook.
function syncvalue.ScreenFade:OnSync( self )
    local value = self.value;
    if value == nil then return; end

    local options = string.explode( value , ';' );
    moduleScreenFade:Draw( options[2] , options[3], options[4] , options[5] , options[6] , options[7] );
end


-- SyncValue chapter OnSync hook.
function syncvalue.Chapter:OnSync( self )
    local value = self.value;
    if value == nil then return; end

    local chapter = string.explode( value , syncvalue.Chapter.Separator );
    moduleChapter:Set( chapter[1] , chapter[2] );
end


-- SyncValue maxhp OnSync hook.
function syncvalue.MaxHealth:OnSync( self )
    local value = self.value;
    if value == nil then return; end

    moduleHealth:Update();
end


-- SyncValue hp OnSync hook.
function syncvalue.Health:OnSync( self )
    local value = self.value;
    if value == nil then return; end

    moduleHealth:Update();
end


-- SyncValue stamina OnSync hook.
function syncvalue.Stamina:OnSync( self )
    local value = self.value;
    if value == nil then return; end

    moduleStamina:Update();
end


-- SyncValue cutscene OnSync hook.
function syncvalue.Cutscene:OnSync( self )
    local value = self.value;
    if value == nil then return; end

    debug:print( "Cutscene:OnSync()" );
    debug:print( "value= " .. tostring(value) );
    moduleCutscene:Set( value );
end


-- SyncValue phone.available OnSync hook.
function syncvalue.Phone.Available:OnSync( self )
    local value = self.value;
    if value == nil then return; end

    debug:print( "Phone.Available:OnSync()" );
    debug:print( "value= " .. tostring(value) );
    modulePhone:SetAvailability( value );
end


-- SyncValue phone.message OnSync hook.
function syncvalue.Phone.Message:OnSync( self )
    local value = self.value;

    debug:print( "Phone.Message:OnSync()" );
    debug:print( "value= " .. tostring(value) );
    modulePhone:SetMessage( value ~= nil and value or "DEFAULT" );
end


-- SyncValue subtitle OnSync hook.
function syncvalue.Subtitle:OnSync( self )
    local value = self.value;
    if value == nil then return; end

    debug:print( "Subtitle:OnSync()" );
    debug:print( "value= " .. tostring(value) );
    moduleSubtitle:Set( value );
end


-- SyncValue hint OnSync hook.
function syncvalue.Hint:OnSync( self )
    local value = self.value;
    if value == nil then return; end

    debug:print( "Hint:OnSync()" );

    value = tostring(value);
    debug:print( "value= " .. value );

    -- Ignores if value is already in queue list.
    for _,msg in ipairs( text.Hint.Config.Queue.Text )
    do
        if msg == value then debug:print( "Already in queue." ); return; end
    end

    moduleHint:Enqueue( value );
end


-- SyncValue objective OnSync hook.
function syncvalue.Objective:OnSync( self )
    local value = self.value;
    --if value == nil then return; end

    moduleObjective:Set( value );
end


-- SyncValue prologue OnSync hook.
function syncvalue.Prologue:OnSync( self )
    local value = self.value;
    if value == nil then return; end

    debug:print( "Prologue:OnSync()" );
    debug:print( "value= " .. tostring(value) );
    modulePrologue:Show( value );
end


-- SyncValue epilogue OnSync hook.
function syncvalue.Epilogue:OnSync( self )
    local value = self.value;
    --if value == nil then return; end

    debug:print( "Epilogue:OnSync()" );
    debug:print( "value= " .. tostring(value) );
    moduleEpilogue:Show( value );
end


-- SyncValue credit OnSync hook.
function syncvalue.Credit:OnSync( self )
    local value = self.value;
    if value == nil then return; end

    debug:print( "Credit:OnSync()" );
    debug:print( "value= " .. tostring(value) );
    moduleCredit:Show( value );
end


-- OnUpdate hook.
function module:OnUpdate( time )
    moduleScreenFade:Update();
    moduleChapter:Update();
    moduleHealth:Update();
    moduleStamina:Update();
    moduleCutscene:Update();
    modulePhone:UpdateOverlay();
    modulePhone:UpdateNewSMSAlert();
    moduleSubtitle:Update();
    moduleHint:Update();
    modulePrologue:Update();
    moduleEpilogue:Update();
    moduleCredit:Update();
end


-- OnInput hook.
function module:OnInput( buttons )
    if isCutscene then return; end
    
    if buttons[UI.KEY.T]
    then
        local time = UI:GetTime();
        local compare = time - lastPressedPhoneToggleTime;
        if compare > (COF.FASTEST_RECEIVED_DOUBLETAP_INPUT_TIME/1000)
        then
            modulePhone:SetVisibility( not isPhoneVisible );
        end
        lastPressedPhoneToggleTime = time;
    end

    if buttons[UI.KEY.B]
    then
        local time = UI:GetTime();
        local compare = time - lastPressedObjectiveToggleTime;
        if compare > (COF.FASTEST_RECEIVED_DOUBLETAP_INPUT_TIME/1000)
        then
            moduleObjective:Show();
        end
        lastPressedObjectiveToggleTime = time;
    end

    -- Disabled because we can't detect player ducking yet. --Anggara_nothing
    -- Use CTRL modifier.
    --[[
    if buttons[UI.KEY.CTRL]
    then
        UI.Signal( enum.SIGNAL.KEY_CTRL );
    end
    ]]

    -- Jump.
    if buttons[UI.KEY.SPACE]
    then
        UI.Signal( enum.SIGNAL.KEY_SPACE );
    end

    -- Use SHIFT modifier.
    if buttons[UI.KEY.SHIFT]
    then
        if buttons[UI.KEY.W]
        then
            UI.Signal( enum.SIGNAL.SPRINT );
            return; -- ignores other keys.
        else
            UI.Signal( enum.SIGNAL.KEY_SHIFT );
        end
    end

    -- Sends signal for double-tap checks.
    if buttons[UI.KEY.A]
    then
        UI.Signal( enum.SIGNAL.KEY_A );
        return; -- ignores other keys.
    end
    if buttons[UI.KEY.D]
    then
        UI.Signal( enum.SIGNAL.KEY_D );
        return; -- ignores other keys.
    end
    if buttons[UI.KEY.S]
    then
        UI.Signal( enum.SIGNAL.KEY_S );
        return; -- ignores other keys.
    end
    if buttons[UI.KEY.W]
    then
        UI.Signal( enum.SIGNAL.KEY_W );
        return; -- ignores other keys.
    end
end


-- OnCutscene hookable event.
function moduleEvent:OnCutscene( onoff )
end
-- OnPrologue hookable event.
function moduleEvent:OnPrologue( onoff )
end
-- OnEpilogue hookable event.
function moduleEvent:OnEpilogue( onoff )
end
-- OnCredit hookable event.
function moduleEvent:OnCredit( onoff )
end


-- Default hook execution(s).
function syncvalue.ScreenFade.Instance:OnSync()
    syncvalue.ScreenFade:OnSync( self );
end
function syncvalue.Chapter.Instance:OnSync()
    syncvalue.Chapter:OnSync( self );
end
function syncvalue.MaxHealth.Instance:OnSync()
    syncvalue.MaxHealth:OnSync( self );
end
function syncvalue.Health.Instance:OnSync()
    syncvalue.Health:OnSync( self );
end
function syncvalue.Cutscene.Instance:OnSync()
    syncvalue.Cutscene:OnSync( self );
end
function syncvalue.Phone.Available.Instance:OnSync()
    syncvalue.Phone.Available:OnSync( self );
end
function syncvalue.Phone.Message.Instance:OnSync()
    syncvalue.Phone.Message:OnSync( self );
end
function syncvalue.Subtitle.Instance:OnSync()
    syncvalue.Subtitle:OnSync( self );
end
function syncvalue.Hint.Instance:OnSync()
    syncvalue.Hint:OnSync( self );
end
function syncvalue.Objective.Instance:OnSync()
    syncvalue.Objective:OnSync( self );
end
function syncvalue.Prologue.Instance:OnSync()
    syncvalue.Prologue:OnSync( self );
end
function syncvalue.Epilogue.Instance:OnSync()
    syncvalue.Epilogue:OnSync( self );
end
function syncvalue.Credit.Instance:OnSync()
    syncvalue.Credit:OnSync( self );
end
function UI.Event:OnUpdate( time )
    module:OnUpdate( time );
end
function UI.Event:OnInput( buttons )
    module:OnInput( buttons );
end


print( "UI.COF.Main is loaded!" );

